package cn.ycoz.cloud.windspeed.action;

import com.alibaba.fastjson.JSON;
import com.alibaba.fastjson.JSONObject;
import org.apache.commons.lang3.BooleanUtils;
import org.apache.commons.lang3.math.NumberUtils;
import org.apache.commons.lang3.reflect.MethodUtils;
import org.springframework.beans.BeansException;
import org.springframework.beans.factory.InitializingBean;
import org.springframework.context.ApplicationContext;
import org.springframework.context.ApplicationContextAware;
import org.springframework.data.domain.Page;
import org.springframework.data.domain.PageRequest;
import org.springframework.data.domain.Sort;
import org.springframework.data.jpa.repository.JpaRepository;
import org.springframework.data.jpa.repository.support.JpaRepositoryFactoryBean;
import org.springframework.data.repository.core.RepositoryInformation;
import org.springframework.data.repository.query.Param;
import org.springframework.data.rest.core.annotation.RepositoryRestResource;
import org.springframework.data.rest.core.annotation.RestResource;
import org.springframework.http.HttpStatus;
import org.springframework.http.ResponseEntity;
import org.springframework.util.MultiValueMap;
import org.springframework.web.bind.annotation.*;

import java.lang.reflect.Method;
import java.lang.reflect.Parameter;
import java.util.*;
import java.util.concurrent.ConcurrentHashMap;

import static org.apache.commons.lang3.StringUtils.isEmpty;
import static org.apache.commons.lang3.exception.ExceptionUtils.getMessage;


@RestController
public class RestfulController implements ApplicationContextAware, InitializingBean {

    private static final String BASE_MAPPING = "/dao/{repository}";

    private ApplicationContext context;

    @ResponseBody
    @RequestMapping(value = BASE_MAPPING, method = RequestMethod.GET)
    public ResponseEntity<String> executeSearch(@RequestParam MultiValueMap<String, Object> parameters,
                                                @PathVariable String repository,
                                                @RequestParam(value = "page", defaultValue = "1") int page,
                                                @RequestParam(value = "rows", defaultValue = "50") int size) {
        ResponseEntity<String> response = new ResponseEntity<>(HttpStatus.INTERNAL_SERVER_ERROR);

        if (repoResources.containsKey(repository)) {
            JpaRepositoryFactoryBean repoFactory = repoResources.get(repository);

            PageRequest pageable = getPageRequest(parameters, repoFactory, page, size);

            Class<?> clazz = repoFactory.getRepositoryInformation().getRepositoryInterface();
            JpaRepository jpaRepo = (JpaRepository) context.getBean(clazz);
            Page result = jpaRepo.findAll(pageable);

            JSONObject jsonObject = new JSONObject();
            jsonObject.put("total", result.getTotalElements());
            jsonObject.put("totalPages", result.getTotalPages());
            jsonObject.put("rows", result.getContent());
            response = new ResponseEntity<>(jsonObject.toJSONString(), HttpStatus.OK);
        }
        return response;
    }

    private PageRequest getPageRequest(MultiValueMap<String, Object> parameters,
                                       JpaRepositoryFactoryBean repoFactory,
                                       int page, int size) {
        String idName = repoFactory.getPersistentEntity().getIdProperty().getName();
        String order = String.valueOf(parameters.getFirst("order"));
        Sort.Direction direction = "desc".equalsIgnoreCase(order) ?
                Sort.Direction.DESC : Sort.Direction.ASC;
        Sort sort = new Sort(direction, idName);
        return new PageRequest(page - 1, size, sort);
    }

    @ResponseBody
    @RequestMapping(value = BASE_MAPPING + "/{search}", method = RequestMethod.GET)
    public ResponseEntity<String> executeSearch(@RequestParam MultiValueMap<String, Object> parameters,
                                                @PathVariable String search,
                                                @PathVariable String repository,
                                                @RequestParam(value = "page", defaultValue = "0") int page,
                                                @RequestParam(value = "size", defaultValue = "50") int size) {

        ResponseEntity<String> response = new ResponseEntity<>(HttpStatus.INTERNAL_SERVER_ERROR);
        if (isEmpty(search)) {
            return response;
        }

        if (repoResources.containsKey(repository)) {
            JpaRepositoryFactoryBean repoFactory = repoResources.get(repository);

            RepositoryInformation repoInfo = repoFactory.getRepositoryInformation();
            Class<?> clazz = repoInfo.getRepositoryInterface();
            Iterable<Method> methods = repoInfo.getQueryMethods();
            JpaRepository jpaRepo = (JpaRepository) context.getBean(clazz);

            PageRequest pageable = getPageRequest(parameters, repoFactory, page, size);

            for (Method m : methods) {
                if (m.isAnnotationPresent(RestResource.class)) {
                    RestResource an = m.getAnnotation(RestResource.class);
                    if (!search.equals(an.rel())) {
                        continue;
                    }

                    Parameter[] params = m.getParameters();
                    Object[] values = new Object[params.length];
                    for (int i = 0; i < params.length; i++) {
                        Parameter param = params[i];
                        String paramName = param.getAnnotation(Param.class).value();

                        Object first = parameters.getFirst(paramName);
                        String val = String.valueOf(first);
                        Class<?> pClazz = param.getType();

                        if (String.class == pClazz) {
                            values[i] = val;
                        } else if (PageRequest.class == pClazz) {
                            values[i] = pageable;
                        } else if (Integer.class == pClazz) {
                            values[i] = NumberUtils.createInteger(val);
                        } else if (Double.class == pClazz) {
                            values[i] = NumberUtils.createDouble(val);
                        } else if (Float.class == pClazz) {
                            values[i] = NumberUtils.createFloat(val);
                        } else if (Long.class == pClazz) {
                            values[i] = NumberUtils.createLong(val);
                        } else if (Boolean.class == pClazz) {
                            values[i] = BooleanUtils.toBoolean(val);
                        } else if (List.class == pClazz) {
                            values[i] = parameters.get(paramName);
                        } else {
                            values[i] = first;
                        }
                    }

                    try {
                        Object result = MethodUtils.invokeMethod(jpaRepo, m.getName(), values, m.getParameterTypes());
                        String str = JSON.toJSONString(result);
                        response = new ResponseEntity<>(str, HttpStatus.OK);
                    } catch (Exception e) {
                        e.printStackTrace();
                        response = new ResponseEntity<>(getMessage(e), HttpStatus.INTERNAL_SERVER_ERROR);
                    }
                }
            }
        }
        return response;
    }


    @Override
    public void setApplicationContext(ApplicationContext applicationContext) throws BeansException {
        this.context = applicationContext;
    }

    @Override
    public void afterPropertiesSet() throws Exception {
        Collection<JpaRepositoryFactoryBean> repositories = context.getBeansOfType(JpaRepositoryFactoryBean.class).values();
        for (JpaRepositoryFactoryBean repo : repositories) {
            String name;
            Class<?> repoInterface = repo.getRepositoryInformation().getRepositoryInterface();
            if (repoInterface.isAnnotationPresent(RepositoryRestResource.class)) {
                name = repoInterface.getAnnotation(RepositoryRestResource.class).path();
            } else {
                Class<?> domain = repo.getRepositoryInformation().getDomainType();
                name = domain.getSimpleName().toLowerCase() + "s";
            }
            repoResources.put(name, repo);
        }
    }

    private final Map<String, JpaRepositoryFactoryBean> repoResources = new ConcurrentHashMap<>();

}